iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0

大家好!我是2魚,今天我們要深入探討後端開發中兩個核心概念:API(應用程式介面)和CRUD(創建、讀取、更新、刪除)操作。這兩個概念是構建現代Web應用的基石。我們將從MongoDB資料庫設置開始,逐步實現一個完整的API,涵蓋所有CRUD操作。無論你是後端新手還是想複習基礎的開發者,這篇文章都會給你實用的洞見和hands-on經驗。讓我們開始這段精彩的後端之旅吧!

1. MongoDB Atlas 環境準備

首先,我們需要確保MongoDB Atlas中的測試環境是乾淨的。這個步驟很重要,因為它可以幫助我們避免舊數據干擾新的開發過程。

  1. 登入MongoDB Compass

  2. 找到你的test資料庫
    https://ithelp.ithome.com.tw/upload/images/20240924/20159686Ef1NSznZiX.png

  3. 刪除所有現有的集合
    https://ithelp.ithome.com.tw/upload/images/20240924/20159686nstyYoDZ87.png
    這樣做可以讓我們從一個乾淨的狀態開始開發,避免之前的測試數據影響我們的新功能開發。

2. 定義Mongoose模型

1. 打開 VsCode,在專案根目錄下創建models資料夾

https://ithelp.ithome.com.tw/upload/images/20240924/20159686LIjyxmdmfD.png

2. 參考我們在第七天規劃的資料表 Excel

https://ithelp.ithome.com.tw/upload/images/20240924/201596864i1252skD9.png

KEY 英文欄位名稱 中文欄位名稱 型態 必填 預設值 備註
PK _id ID ObjectID v
contactPerson 名稱 String v
phone 電話 String
email 信箱 String v
content 內容 String v
source 從哪裡得知本網站 Number 1:網路搜尋 2:親友推薦 3:社群媒體 4:其他
createTime 創建時間 Date now

3. 在 models 資料夾內新增feedback.js文件

https://ithelp.ithome.com.tw/upload/images/20240924/201596861WiijIyO7A.png

  1. 引入 Mongoose 套件

    const mongoose = require("mongoose");
    

    這行程式碼的作用是引入 mongoose 這個工具,它能幫助我們與 MongoDB 資料庫進行溝通,並且管理資料。

  2. 定義聯絡我們模型

    const feedbackSchema = new mongoose.Schema(
      {
        // 名稱
        contactPerson: { type: String, required: [true, "姓名未填寫"] },
        // 電話
        phone: { type: String },
        // 信箱
        email: { type: String, required: [true, "信箱未填寫"] },
        // 內容
        feedback: { type: String, required: [true, "內容未填寫"] },
        // 從哪裡得知此網站
        source: {
          type: String,
          enum: ["網路搜尋", "社群媒體", "親友介紹", "其他"],
        },
      },
      {
        versionKey: false,
        timestamps: true,
      }
    );
    

    這段程式碼的目的是創建一個「模型」,用來描述我們想在資料庫裡儲存的「聯絡我們」資料的結構,簡而言之,就是這些資料的「樣板」。

    每個欄位的說明:
    - contactPerson (名稱):用於儲存用戶的名字。type: String 表示它是文字格式,而且必須填寫(required: true),如果沒填寫就會顯示「姓名未填寫」的錯誤訊息。
    - phone (電話):用於儲存用戶的電話號碼。它是文字格式,但這個欄位可以不填(沒有 required)。
    - email (信箱):用於儲存用戶的電子郵件地址。它也是文字格式,而且必須填寫,否則會顯示「信箱未填寫」的錯誤訊息。
    - feedback (內容):用於儲存用戶給你的反饋內容。這也是必填項,沒填寫的話會顯示「內容未填寫」。
    - source (從哪裡得知此網站):用於記錄用戶是從哪裡知道你這個網站的。它只能是幾個選項之一(例如「網路搜尋」、「社群媒體」等)。

    其他設定:
    - versionKey: false:這個設定讓 Mongoose 不會自動生成一個叫 __v 的版本號欄位。
    - timestamps: true:這個設定會自動幫你添加兩個欄位,分別記錄資料創建和最後更新的時間。

  3. 創建並導出模型

    const Feedback = mongoose.model("Feedback", feedbackSchema);
    module.exports = Feedback;
    

    最後,我們使用 mongoose.model 創建了一個名為 Feedback 的模型,這個模型就像是我們剛剛定義的資料「樣板」的具體實現。
    接著,我們用 module.exports 將這個模型導出,這樣在其他地方的程式碼中也可以使用它。

4. 掛載到 app.js 進行測試

  • 打開 app.js 添加以下程式碼:
    const Feedback = require("./models/feedback");
    
  • 開啟終端機,輸入 npm start 執行專案
  • 回到 compass 可以看到 test 資料庫多出一個新的 feedbacks 集合
    https://ithelp.ithome.com.tw/upload/images/20240924/201596866yv8AeBEgs.png

💡 補充說明:為什麼新增的是 feedbacks 而不是 feedback
當你使用 Mongoose 創建模型時,Mongoose 會自動將模型名稱轉換為複數形式,用來命名 MongoDB 中的集合。這是 Mongoose 的預設行為。
### 在上面的範例:
- 我們定義了一個模型叫 Feedback
- Mongoose 會把 Feedback 變成 feedbacks,然後在資料庫裡建立這個集合。
- 所以,當我們把資料存進去時,它會自動進入 feedbacks 這個集合,而不是 feedback
這樣的設計是因為資料庫通常會存放多筆資料,所以用複數形式來命名會更符合實際情況。
### 如果你不想要複數形式:
如果你希望集合名稱保持單數,你可以在創建模型時指定具體的集合名稱:

```jsx
// 這是上面範例寫法,建立出來的集合會是 feedbacks
const Feedback = mongoose.model("Feedback", feedbackSchema); 

// 自訂集合名稱,撰寫在 () 第三個變數,建立出來會是 feedback
const Feedback = mongoose.model("Feedback", feedbackSchema, "feedback"); 
```
這樣資料就會存到名為 `feedback` 的集合裡,而不是 `feedbacks`。
這種自動複數化的行為是 Mongoose 幫你做的,但你也可以輕鬆地控制它。

開發第一個 API:Feedback CRUD 操作

接下來,我們將在 Express 專案中實現 CRUD(創建、讀取、更新、刪除)操作。
我們會以上面定義的 Feedback 模型為例,直接在 routes 中實現這些功能。

Step1. 再次檢視我們的 Feedback 模型:

const mongoose = require("mongoose");

const feedbackSchema = new mongoose.Schema(
  {
    contactPerson: { type: String, required: [true, "姓名未填寫"] },
    phone: { type: String },
    email: { type: String, required: [true, "信箱未填寫"] },
    feedback: { type: String, required: [true, "內容未填寫"] },
    source: {
      type: String,
      enum: ["網路搜尋", "社群媒體", "親友介紹", "其他"],
    },
  },
  {
    versionKey: false,
    timestamps: true,
  }
);

const Feedback = mongoose.model("Feedback", feedbackSchema);

module.exports = Feedback;

Step 2. 在 routes 資料夾中創建 feedback.js 檔案,並實現 CRUD 操作:

首先,我們需要設置基本的路由結構:

const express = require("express");
const router = express.Router();
const Feedback = require("../models/feedback");

// 這裡我們將添加我們的 CRUD 操作

module.exports = router;

現在,讓我們逐一實現 CRUD 操作:

  1. 新增 feedback

    // 新增 feedback
    router.post("/", async (req, res) => {
      try {
        // 從請求的 body 中提取出用戶提交的資料(姓名、電話、信箱、反饋內容)
        const { contactPerson, phone, email, feedback } = req.body;
    
        // 驗證必填欄位,檢查是否提供了姓名、信箱和反饋內容
        if (!contactPerson || !email || !feedback) {
          // 如果缺少必填欄位,回應 400 錯誤狀態和錯誤信息
          return res.status(400).json({
            status: "fail",
            message: "姓名、信箱、內容為必填欄位",
          });
        }
    
        // 如果必填欄位都有,使用 Feedback 模型創建一條新的反饋記錄
        const newFeedback = await Feedback.create({
          contactPerson: contactPerson,  // 設置聯絡人姓名
          phone: phone,  // 設置聯絡人電話(可選)
          email: email,  // 設置聯絡人信箱
          feedback: feedback,  // 設置反饋內容
        });
    
        // 成功創建後,回應 201 狀態(已創建)和成功信息,包含新創建的反饋數據
        res.status(201).json({
          status: "success",
          data: newFeedback,
        });
      } catch (error) {
        // 如果在創建過程中發生錯誤,回應 400 錯誤狀態和錯誤信息
        res.status(400).json({
          status: "fail",
          message: error.message,
        });
      }
    });
    

    詳細解釋:

    1. 路由設置router.post("/"...) 表示這是一個處理POST請求的路由,路徑為根路徑"/"。
    2. 非同步處理:使用async/await來處理非同步操作,確保資料庫操作完成後再回應。
    3. 解構請求體const { contactPerson, phone, email, feedback } = req.body; 從請求體中提取所需資料。
    4. 必填欄位驗證:檢查必填欄位是否存在,如果缺少則返回400錯誤。
    5. 創建新記錄:使用Feedback.create()方法創建新的feedback記錄。
    6. 成功回應:如果創建成功,返回201狀態碼和新創建的資料。
    7. 錯誤處理:如果過程中出現錯誤,捕獲錯誤並返回400錯誤狀態。
  2. 獲取所有 Feedback

    // 取得所有 feedback
    router.get("/", async (req, res) => {
      try {
        // 使用 find() 方法從資料庫中取得所有 feedbacks
        const feedbacks = await Feedback.find();
    
        // 成功取得資料後,回應 200 狀態碼和回傳的資料
        res.status(200).json({
          status: "success", // 狀態為成功
          results: feedbacks.length, // 回傳的 feedbacks 數量
          data: feedbacks, // 回傳所有 feedbacks 的資料
        });
      } catch (error) {
        // 如果發生錯誤(如資料庫連接問題),回應 404 錯誤狀態和錯誤訊息
        res.status(404).json({
          status: "fail", // 狀態為失敗
          message: error.message, // 返回錯誤訊息
        });
      }
    });
    

    詳細解釋:

    1. GET請求處理:這個路由處理GET請求,用於獲取所有feedback。
    2. 查詢所有記錄Feedback.find()方法不帶參數,會返回所有feedback記錄。
    3. 回應結構:回應包含狀態、結果數量和實際數據,為前端提供更多信息。
    4. 錯誤處理:如果查詢過程出錯,返回404錯誤。
  3. 獲取指定 ID 的 Feedback

    // 取得指定 id feedback
    router.get("/:id", async (req, res) => {
      try {
        // 使用 findById 方法從資料庫中取得指定 id 的 feedback
        const feedback = await Feedback.findById(req.params.id);
    
        // 如果找不到該 id 對應的資料,回應 404 錯誤狀態和錯誤訊息
        if (!feedback) {
          return res.status(404).json({
            status: "fail", // 狀態為失敗
            message: "找不到該 feedback", // 返回錯誤訊息
          });
        }
    
        // 成功取得資料後,回應 200 狀態碼和回傳的資料
        res.status(200).json({
          status: "success", // 狀態為成功
          data: feedback, // 回傳指定 id 的 feedback 資料
        });
      } catch (error) {
        // 如果發生錯誤(如無效的 id),回應 404 錯誤狀態和錯誤訊息
        res.status(404).json({
          status: "fail", // 狀態為失敗
          message: error.message, // 返回錯誤訊息
        });
      }
    });
    

    詳細解釋:

    1. 路徑參數:id是一個路徑參數,可以通過req.params.id獲取。
    2. 查找特定記錄:使用Feedback.findById()方法查找特定ID的feedback。
    3. 記錄不存在處理:如果找不到對應記錄,返回404錯誤。
    4. 成功回應:找到記錄後,返回200狀態碼和記錄數據。
  4. 更新指定 ID 的 Feedback

    // 更新指定 id feedback
    router.patch("/:id", async (req, res) => {
      try {
        // 檢查必填欄位是否存在
        const { contactPerson, email, feedback } = req.body;
        if (!contactPerson || !email || !feedback) {
          return res.status(400).json({
            status: "fail",
            message: "姓名、信箱、內容為必填欄位",
          });
        }
    
        // 使用 findByIdAndUpdate 更新資料
        const updatedFeedback = await Feedback.findByIdAndUpdate(
          req.params.id, // 從 URL 參數中獲取要更新的資料 ID
          req.body, // 使用請求 body 中的資料進行更新
          {
            new: true, // 返回更新後的資料
            runValidators: true, // 在更新時執行驗證
          }
        );
    
        // 如果找不到該資料,回應 404 錯誤
        if (!updatedFeedback) {
          return res.status(404).json({
            status: "fail",
            message: "找不到該 feedback",
          });
        }
    
        // 成功更新後回應 200 狀態和更新後的資料
        res.status(200).json({
          status: "success",
          data: updatedFeedback,
        });
      } catch (error) {
        // 捕獲錯誤並返回 400 錯誤狀態和錯誤信息
        res.status(400).json({
          status: "fail",
          message: error.message,
        });
      }
    });
    

    詳細解釋:

    1. PATCH請求:使用PATCH方法允許部分更新資源。
    2. 必填欄位驗證:再次檢查必填欄位,確保更新操作的完整性。
    3. 更新操作findByIdAndUpdate方法同時查找和更新文檔。
      • new: true選項返回更新後的文檔。
      • runValidators: true確保更新時執行模型的驗證規則。
    4. 更新失敗處理:如果找不到要更新的文檔,返回404錯誤。
    5. 成功回應:更新成功後,返回更新後的文檔。
  5. 刪除指定 ID 的 Feedback

    // 刪除指定 id feedback
    router.delete("/:id", async (req, res) => {
      try {
        // 使用 findByIdAndDelete 根據 URL 中的 id 參數查找並刪除對應的 feedback
        const feedback = await Feedback.findByIdAndDelete(req.params.id);
    
        // 如果找不到對應的 feedback(即該 id 不存在),回應 404 錯誤
        if (!feedback) {
          return res.status(404).json({
            status: "fail", // 狀態為失敗
            message: "找不到該 feedback", // 返回錯誤訊息
          });
        }
    
        // 如果成功刪除,回應 200 狀態碼,表示成功操作
        res.status(200).json({
          status: "success", // 狀態為成功
          message: "刪除成功", // 返回成功訊息
          data: null, // 雖然此處用 200,但不返回具體的資料,data 設為 null
        });
      } catch (error) {
        // 如果發生錯誤(例如無效的 id),回應 400 錯誤狀態
        res.status(400).json({
          status: "fail", // 狀態為失敗
          message: error.message, // 返回錯誤訊息,通常是錯誤的詳細信息
        });
      }
    });
    

    詳細解釋:

    1. 刪除操作:使用findByIdAndDelete方法查找並刪除指定ID的文檔。
    2. 刪除失敗處理:如果找不到要刪除的文檔,返回404錯誤。
    3. 成功回應:刪除成功後,返回200狀態碼。注意這裡返回data: null,因為資源已被刪除。
    4. 錯誤處理:捕獲可能的錯誤(如無效ID)並返回400錯誤。
  6. 最後,別忘了在 app.js 中引入這個新的路由:

    const feedbackRoute = require("./routes/feedback");
    
    app.use("/feedbacks", feedbackRoute);
    

    https://ithelp.ithome.com.tw/upload/images/20240924/20159686idBwmleeX9.png
    這樣,我們就完成了 Feedback 的 CRUD 操作!每個操作都有其特定的 HTTP 方法和路徑,使用了適當的 Mongoose 方法來操作數據庫。


API常用HTTP狀態碼

在開發API時,正確使用HTTP狀態碼對於前後端溝通至關重要。以下是一些常用的狀態碼及其含義:

  1. 200 OK:請求成功。通常用於GET和PUT/PATCH請求。
  2. 201 Created:請求成功並創建了新資源。常用於POST請求。
  3. 204 No Content:請求成功,但無返回內容。常用於DELETE請求。
  4. 400 Bad Request:客戶端請求有誤,例如請求體缺少必要參數。
  5. 401 Unauthorized:未經授權的請求,通常是因為缺少或無效的認證。
  6. 403 Forbidden:伺服器理解請求但拒絕執行,通常是權限問題。
  7. 404 Not Found:請求的資源不存在。
  8. 500 Internal Server Error:伺服器遇到未知錯誤,無法完成請求。
    正確使用這些狀態碼可以幫助客戶端更好地理解請求的結果,提高API的可用性和可維護性。

HTTP狀態碼速查表

狀態碼 說明 使用情境 餐廳情境比喻
200 OK 請求成功,資料已回傳。 GET取得資料、PUT/PATCH更新資料成功時。 服務生:「您的餐點已送達,請慢用。」
201 Created 請求成功,新資源已建立。 POST新增資料成功時。 櫃台:「您的訂位已成功,座位已準備好。」
204 No Content 請求成功,但無需回傳資料。 刪除成功,或不需回傳內容的操作。 服務生收走空盤子,點頭示意但不說話。
400 Bad Request 請求有誤,伺服器無法處理。 傳送的資料格式錯誤或缺少必要參數。 廚師:「這個訂單資訊不完整,請確認後再下單。」
401 Unauthorized 需要驗證身份。 未登入或登入已過期。 櫃台:「對不起,這是會員專屬餐廳,請出示會員卡。」
403 Forbidden 已驗證身份,但權限不足。 嘗試存取無權限的資源。 經理:「很抱歉,員工專用廚房區域不對外開放。」
404 Not Found 請求的資源不存在。 訪問不存在的網址或資源。 服務生:「對不起,我們的菜單上沒有這道菜。」
500 Internal Server Error 伺服器內部錯誤。 伺服器端程式錯誤或未預期的異常。 主廚:「非常抱歉,廚房設備突然故障,暫時無法處理訂單。」

為什麼正確使用狀態碼很重要?

正確使用 HTTP 狀態碼能大幅提升 API 的可用性和可維護性。它不只幫助開發者快速理解請求的結果,也有助於前端更有效地處理不同情況。選擇適當的狀態碼,就像在 API 和使用者之間建立了一個清晰的溝通橋樑。
記住,優秀的 API 設計不僅僅是實現功能,更重要的是提供清晰的溝通。適當使用狀態碼,就像為您的 API 安裝了一個高效的訊息系統,讓每次的請求和回應都能一目了然,大大提升了開發效率和用戶體驗。

💡 輕鬆搞懂:HTTP狀態碼 200 OK 和 204 No Content 的差別

在處理API刪除操作時常見的兩種狀態碼:200 OK 和 204 No Content。它們到底有啥不一樣?來,讓我給你講個簡單的例子:

200 OK:「欸,搞定了啦!來,我跟你說說細節」

想像你請你的好麻吉幫忙刪掉一張尷尬的路邊攤醉倒照片。你麻吉不只幫你刪掉,還很熱心地跟你說:「搞定囉!那張照片是上個月吃宵夜時拍的,檔案有5MB大欸!」
在API裡面,用200 OK就是這種感覺,不只說刪掉了,還會給你一堆有的沒的:

    res.status(200).json({
      message: "刪掉囉,放心啦",
      deletedItem: {
        name: "醉倒路邊攤.jpg",
        date: "2023-05-01",
        size: "5MB"
      }
    });

204 No Content:「搞定了啦,就這樣」

現在想像你的麻吉只是簡單回你:「OK啦」,什麼都沒多說。
在API裡,用204 No Content就是醬子的:

    res.status(204).send();

這基本上就是跟客戶端說:「刪掉了啦,沒事了,掰掰」。

什麼時候用哪個咧?

  1. 用 200 OK 的時機:
    - 你想跟前端多嘴幾句,講講刪掉什麼東西
    - 前端需要這些資訊來更新畫面
  2. 用 204 No Content 的時機:
    - 你只想說「刪掉了啦」,不想多講
    - 你想省點網路流量(因為204的回應通常是空的)

實際一點的例子

  • PTT刪除推文(用204)
        app.delete('/posts/:id/comments/:commentId', (req, res) => {
          // 刪除推文的程式碼
          res.status(204).send();
        });

這裡啊,我們只要知道推文刪掉了就好,不用知道更多有的沒的。

  • 刪除雲端硬碟的檔案(用200)
        app.delete('/files/:id', (req, res) => {
          // 刪除檔案的程式碼
          res.status(200).json({
            message: "檔案刪掉囉",
            deletedFile: {
              name: "期末報告.docx",
              size: "2.5MB",
              lastEdited: "2023-06-15"
            }
          });
        });

這個例子中,回傳被刪掉的檔案資訊可能對使用者有幫助,萬一誤刪還可以知道刪掉什麼。

總結

  • 200 OK:「搞定了啦,而且我還知道這個那個」
  • 204 No Content:「搞定了啦,沒什麼好講的」
    選哪個主要看你的API到底要不要在刪除後多嘴幾句。記住喔,保持一致很重要 - 你的API決定要用哪種方式就要貫徹始終,不要三心二意喔!

結語

好啦!我們的 API 小冒險暫時告一段落。從資料庫設置到 CRUD 操作,我們跑了一圈後端開發的小obstacle course。現在的你,就像剛學會騎腳踏車的小孩,已經可以自己平穩前進了!
記住,一個好的 API 就像一個好朋友:容易相處(易用性)、守信用(可靠性),而且總是能給出清楚的回應(狀態碼和錯誤訊息)。你現在已經認識了這位新朋友,接下來就是多多練習,讓你們的友誼更加深厚。
下一站,我們要認識一位新朋友:Postman。它就像是你的 API 健身教練,幫你檢查每個動作是否到位。準備好要再次揮灑汗水了嗎?
繼續保持熱情,開發的世界總有新奇等著你。現在,是時候歇口氣,喝杯咖啡,然後準備迎接下一個挑戰了!
大家有沒有想要更詳細補充的部分呢?歡迎在下方留言分享喔!我們一起學習、一起成長。明天見啦,掰掰~
(對了,如果你覺得今天的內容對你有幫助,別忘了給個讚支持一下喔!這會是我繼續努力的動力呢~)


上一篇
[ DAY14 ] 串接專案與雲端資料庫:MongoDB Atlas 連接與 Git 複習
下一篇
[ DAY16 ] Postman 初體驗:API 測試小白入門指南
系列文
房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言